package org.zbclan.bf2.rcon.common.server;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Hashtable;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.utils.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zbclan.bf2.rcon.common.shared.Bf2Object;
import org.zbclan.bf2.rcon.common.shared.Bf2Property;
import org.zbclan.bf2.rcon.common.shared.CombatArea;
import org.zbclan.bf2.rcon.common.shared.CombatAreaManager;
import org.zbclan.bf2.rcon.common.shared.ControlPoints;
import org.zbclan.bf2.rcon.common.shared.GameLogic;
import org.zbclan.bf2.rcon.common.shared.GamePlayObject;
import org.zbclan.bf2.rcon.common.shared.LevelException;
import org.zbclan.bf2.rcon.common.shared.ObjectSpawner;
import org.zbclan.bf2.rcon.common.shared.ObjectTemplate;
import org.zbclan.bf2.rcon.common.shared.SpawnPoints;

public class ServerZip {

	// public static final String GPO_REGEX =
	// "(?i)(?<=^GameModes/).*(?=/GamePlayObjects.con$)";
	public static final String GPO_REGEX = "(?i)^GameModes/.*/GamePlayObjects.con$";
	public static final Pattern GPO_PATTERN = Pattern.compile(GPO_REGEX);

	private static final Logger logger = LoggerFactory
			.getLogger(ServerZip.class);

	private static final String[] OBJECT_TEMPLATE_PROPERTIES_1 = new String[] {
			"modifiedByUser", "setNetworkableInfo", "isNotSaveable",
			"setCollisionMesh", "mapMaterial 0", "mapMaterial 1",
			"treeCollisionDiameter", "hasMobilePhysics", "hasCollisionPhysics",
			"physicsType" };

	private static final String[] OBJECT_TEMPLATE_PROPERTIES_2 = new String[] {
			"setControlPointName", "radius", "radiusOffset",
			"setObjectTemplate 1", "setObjectTemplate 2", "minSpawnDelay",
			"maxSpawnDelay", "spawnDelayAtStart", "TimeToLive", "Distance",
			"holdObject", "maxNrOfObjectSpawned", "setSpawnPositionOffset",
			"setSpawnPreventionDelay", "setGroup", "setControlPointId",
			"setAIEnterOnSpawn", "setOnlyForAI", "setAllowSpawnCloseToVehicle",
			"setOnlyForHuman", "team", "teamOnVehicle", "controlPointId",
			"areaValueTeam1", "areaValueTeam2", "timeToGetControl",
			"timeToLoseControl", "supplyGroupNeeded 1", "supplyGroupNeeded 2",
			"loseControlWhenEnemyClose", "hasGreyCapturingState",
			"isHemisphere", "unableToChangeTeam", "minimapNameOffset",
			"hoistFlag", "supplyGroupId", "hoistMinMax" };

	private static final String[] OBJECT_PROPERTIES = new String[] {
			Bf2Object.Properties.ABSOLUTE_POSITION.toString(),
			Bf2Object.Properties.ROTATION.toString(),
			Bf2Object.Properties.SET_CONTROL_POINT_ID.toString(),
			Bf2Object.Properties.LAYER.toString() };

	/**
	 * key: path, value: binary
	 */
	private final Hashtable<String, byte[]> arbitraryData;

	/**
	 * key: path, value: GamePlayObject
	 */
	private final Hashtable<String, GamePlayObject> gamePlayObjects;

	private final String levelName;

	private String line;

	public ServerZip(String levelName) throws IOException {

		gamePlayObjects = new Hashtable<String, GamePlayObject>();
		arbitraryData = new Hashtable<String, byte[]>();
		this.levelName = levelName;

		boolean loadGamePlayObjects = true;
		boolean loadArbitraryData = false;
		load(loadGamePlayObjects, loadArbitraryData);

		return;
	}

	private void load(boolean loadGamePlayObject, boolean loadArbitraryData)
			throws IOException {

		// File levelDir = new File(System.getProperty(Options.BF2_LEVELS_DIR),
		// levelName);
		File levelDir = new File(Options.BF2_LEVELS_DIR, levelName);

		ZipArchiveInputStream zipInput = new ZipArchiveInputStream(
				new BufferedInputStream(new FileInputStream(new File(levelDir,
						"server.zip"))));

		try {
			ZipArchiveEntry zipArchiveEntry = zipInput.getNextZipEntry();
			while (zipArchiveEntry != null) {

				String name = zipArchiveEntry.getName();
				logger.trace(name);

				Matcher matcher = GPO_PATTERN.matcher(name);
				if (matcher.matches()) {
					if (loadGamePlayObject) {
						gamePlayObjects.put(name, loadGamePlayObject(zipInput));
					}
				} else {
					if (loadArbitraryData) {
						ByteArrayOutputStream baos = new ByteArrayOutputStream();
						IOUtils.copy(zipInput, baos);
						arbitraryData.put(name, baos.toByteArray());
					}
				}

				zipArchiveEntry = zipInput.getNextZipEntry();
			}
		} finally {
			zipInput.close();
		}
	}

	private GamePlayObject loadGamePlayObject(ZipArchiveInputStream zipInput)
			throws IOException {

		GamePlayObject o = new GamePlayObject();

		BufferedReader r = new BufferedReader(new InputStreamReader(zipInput));
		try {

			line = r.readLine();

			o.setGameLogic(loadGameLogic(r));
			o.setObjectSpawner(loadObjectSpawner(r));
			o.setSpawnPoints(loadSpawnPoints(r));
			o.setControlPoints(loadControlPoints(r));
			o.setCombatAreaManager(loadCombatAreaManager(r));

		} finally {
			// r.close();
		}

		return o;
	}

	private GameLogic loadGameLogic(BufferedReader r) throws IOException {

		GameLogic gpl = new GameLogic();

		while (line != null) {
			if (line.startsWith("gameLogic.")) {
				String[] s = line.substring("gameLogic.".length()).split(" ");
				String prop = s[0];
				if ("setTeamDropVehicle".equals(prop)) {
					String team = s[1];
					if ("1".equals(team)) {
						gpl.setSetTeamDropVehicle1(s[2]);
					} else if ("2".equals(team)) {
						gpl.setSetTeamDropVehicle2(s[2]);
					} else {
						throw new LevelException("unexpected team for"
								+ " gameLogic.setTeamDropVehicle: " + team);
					}
				}
			} else if (line.length() == 0) {
				/* skips */
			} else {
				break;
			}

			line = r.readLine();

		}

		return gpl;
	}

	private ObjectSpawner loadObjectSpawner(BufferedReader r)
			throws IOException {

		if (!"rem ********** Object Spawner **********".equals(line)) {
			throw new LevelException("expected object spawner");
		}

		ObjectSpawner os = new ObjectSpawner();

		line = r.readLine();
		while (line != null) {

			if (line.startsWith("rem [ObjectSpawnerTemplate: ")) {
				os.getTemplates().add(loadTemplate(r));
			} else if (line.length() == 0) {
				/* skips */
			} else {
				break;
			}

			line = r.readLine();
		}

		if (!"if v_arg1 == host".equals(line)) {
			throw new LevelException("expected if");
		}

		line = r.readLine();
		while (line != null) {
			if (line.length() == 0) {
				/* skips blank lines */
			} else {
				break;
			}
			line = r.readLine();
		}

		while (line != null) {
			if (line.startsWith("   rem [ObjectSpawner: ")) {
				os.getObjects().add(loadObject(r));
			} else {
				break;
			}

			line = r.readLine();
		}

		if (!"endIf".equals(line)) {
			throw new LevelException("expected endIf");
		}

		line = r.readLine();
		while (line != null) {
			if (line.length() == 0) {
				/* skips blank lines */
			} else {
				break;
			}
			line = r.readLine();
		}

		return os;
	}

	/**
	 * leaves with 'rem ********** Control Points **********'
	 * 
	 * @param r
	 * @return
	 * @throws IOException
	 */
	private SpawnPoints loadSpawnPoints(BufferedReader r) throws IOException {

		if (!"rem ********** Spawn Points **********".equals(line)) {
			throw new LevelException("expected spawn points");
		}

		SpawnPoints sp = new SpawnPoints();

		line = r.readLine();
		while (line != null) {
			if (line.startsWith("rem [SpawnPointTemplate: ")) {
				sp.getTemplates().add(loadTemplate(r));

				line = r.readLine();
				if (line == null || !line.startsWith("rem [SpawnPoint: ")) {
					throw new LevelException(
							"expected spawn point object creation");
				}

				sp.getObjects().add(loadObject(r));

			} else {
				break;
			}

			line = r.readLine();

		}

		return sp;
	}

	private ControlPoints loadControlPoints(BufferedReader r)
			throws IOException {

		if (!"rem ********** Control Points **********".equals(line)) {
			throw new LevelException("expected control points");
		}

		ControlPoints cp = new ControlPoints();

		line = r.readLine();
		while (line != null) {

			if (line.startsWith("rem [ControlPointTemplate: ")) {
				cp.getTemplates().add(loadTemplate(r));
			} else if (line.length() == 0) {
				/* skips */
			} else {
				break;
			}

			line = r.readLine();
		}

		if (!"if v_arg1 == host".equals(line)) {
			throw new LevelException("expected if");
		}

		line = r.readLine();
		while (line != null) {
			if (line.length() == 0) {
				/* skips blank lines */
			} else {
				break;
			}
			line = r.readLine();
		}

		while (line != null) {
			if (line.startsWith("   rem [ControlPoint: ")) {
				cp.getObjects().add(loadObject(r));
			} else if (line.length() == 0) {
				/* skips */
			} else {
				break;
			}

			line = r.readLine();
		}

		if (!"endIf".equals(line)) {
			throw new LevelException("expected endIf");
		}

		line = r.readLine();
		while (line != null) {
			if (line.length() == 0) {
				/* skips blank lines */
			} else {
				break;
			}
			line = r.readLine();
		}

		return cp;
	}

	private CombatAreaManager loadCombatAreaManager(BufferedReader r)
			throws IOException {

		CombatAreaManager cam = new CombatAreaManager();

		while (line != null) {
			if (line.startsWith("CombatAreaManager.")) {
				String[] s = line.substring("CombatAreaManager.".length())
						.split(" ");
				String name = s[0];
				String value = s[1];
				if (name.equals("use")) {
					cam.setUse(value);
				} else if (name.equals("damage")) {
					cam.setDamage(value);
				} else if (name.equals("timeAllowedOutside")) {
					cam.setTimeAllowedOutside(value);
				} else {
					throw new LevelException(
							"unexpected combat area manager property: " + name);
				}
			} else if (line.length() == 0) {
				/* skips */
			} else {
				break;
			}

			line = r.readLine();
		}

		CombatArea ca = null;

		while (line != null) {
			if (line.startsWith("CombatArea.")) {

				String[] s = line.substring("CombatArea.".length()).split(" ");
				String name = s[0];
				String value = s[1];
				if (name.equals("create")) {

					ca = new CombatArea();
					cam.getAreas().add(ca);

					ca.setCreate(value);

				} else if (name.equals("min")) {
					ca.setMin(value);
				} else if (name.equals("max")) {
					ca.setMax(value);
				} else if (name.equals("addAreaPoint")) {
					ca.getAreaPoints().add(value);
				} else if (name.equals("team")) {
					ca.setTeam(value);
				} else if (name.equals("vehicles")) {
					ca.setVehicles(value);
				} else if (name.equals("layer")) {
					ca.setLayer(value);
				} else if (name.equals("usedByPathFinding")) {
					ca.setUsedByPathFinding(value);
				} else {
					throw new LevelException("unexpected property: " + name);
				}

			} else if (line.length() == 0) {
				/* skips */
			} else {
				throw new LevelException("combat area expected");
			}

			line = r.readLine();
		}

		return cam;
	}

	/**
	 * leaves with an empty line
	 * 
	 * @param r
	 * @return
	 * @throws IOException
	 */
	private ObjectTemplate loadTemplate(BufferedReader r) throws IOException {

		ObjectTemplate t = new ObjectTemplate();

		while (line != null) {

			/* TODO watch out for indent */

			if (line.startsWith("ObjectTemplate.")) {

				int p = line.indexOf(" ");

				String name = line.substring(0, p);
				if ("ObjectTemplate.setObjectTemplate".equals(name)
						|| "ObjectTemplate.supplyGroupNeeded".equals(name)
						|| "ObjectTemplate.create".equals(name)
						|| "ObjectTemplate.activeSafe".equals(name)
						|| "ObjectTemplate.mapMaterial".equals(name)) {
					p = line.indexOf(" ", p + 1);
					name = line.substring(0, p);
				}

				String value = line.substring(p + 1);

				// String name;
				// String value;
				// String[] s =
				// line.substring("ObjectTemplate.".length()).split(
				// " ");
				// if (s.length == 2) {
				// name = s[0];
				// value = s[1];
				// } else if (s.length == 3) {
				// name = s[0] + " " + s[1];
				// value = s[2];
				// } else {
				// throw new LevelException("unexpected property line: "
				// + line);
				// }

				t.addProperty(new Bf2Property(name, value));

			} else if (line.startsWith("rem ")) {
				/* skips rem ------------------------------------- */
				// } else if (line.length() == 0) {
				// break;
			} else {
				// throw new LevelException("object template expected");
				break;
			}

			line = r.readLine();
		}

		return t;
	}

	private Bf2Object loadObject(BufferedReader r) throws IOException {

		Bf2Object o = new Bf2Object();
		boolean enabled = false;

		line = r.readLine();
		while (line != null) {
			String trimmed = line.trim();
			if (trimmed.startsWith("Object.")) {
				String[] s = trimmed.substring("Object.".length()).split(" ");
				String name = s[0];
				String value = s[1];
				o.addProperty(new Bf2Property(name, value));
				enabled = true;
			} else if (trimmed.startsWith("rem Object.")) {
				String[] s = trimmed.substring("rem Object.".length()).split(
						" ");
				String name = s[0];
				String value = s[1];
				o.addDisabledProperty(new Bf2Property(name, value));
			} else if (trimmed.startsWith("rem ")) {
				/*
				 * skip
				 */
			} else {
				break;
			}

			line = r.readLine();
		}

		o.setEnabled(enabled);

		return o;
	}

	/**
	 * @throws FileNotFoundException
	 *             if the file exists but is a directory rather than a regular
	 *             file, does not exist but cannot be created, or cannot be
	 *             opened for any other reason
	 * @throws IOException
	 */
	public void save() throws FileNotFoundException, IOException {

		boolean loadGamePlayObjects = false;
		boolean loadArbitraryData = true;
		load(loadGamePlayObjects, loadArbitraryData);

		// File levelDir = new File(System.getProperty(Options.BF2_LEVELS_DIR),
		// levelName);
		File levelDir = new File(Options.BF2_LEVELS_DIR, levelName);

		/*
		 * backup
		 */
		File targetFile = new File(levelDir, "server.zip");
		File backupFile = new File(levelDir, "server~0.zip");
		if (!backupFile.exists()) {
			if (!targetFile.renameTo(backupFile)) {
				throw new FileNotFoundException("cannot make backup of "
						+ targetFile.getPath());
			}
		}

		ZipArchiveOutputStream zipOutput = new ZipArchiveOutputStream(
				new BufferedOutputStream(new FileOutputStream(targetFile)));
		try {
			for (Entry<String, byte[]> e : arbitraryData.entrySet()) {
				ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(
						e.getKey());
				zipOutput.putArchiveEntry(zipArchiveEntry);
				IOUtils.copy(new ByteArrayInputStream(e.getValue()), zipOutput);
				zipOutput.closeArchiveEntry();
			}
			for (Entry<String, GamePlayObject> e : gamePlayObjects.entrySet()) {
				ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(
						e.getKey());
				zipOutput.putArchiveEntry(zipArchiveEntry);
				// IOUtils.copy(new ByteArrayInputStream(e.getValue()),
				// zipOutput);
				saveGamePlayObject(e.getValue(), zipOutput);
				zipOutput.closeArchiveEntry();
			}
		} finally {
			zipOutput.close();
		}

		/*
		 * clear arbitrary data
		 */
		arbitraryData.clear();

		return;
	}

	private void saveGamePlayObject(GamePlayObject o,
			ZipArchiveOutputStream zipOutput) throws IOException {

		/* default encoding is utf-8 */
		BufferedWriter w = new BufferedWriter(new OutputStreamWriter(zipOutput));
		try {

			saveGameLogic(o.getGameLogic(), w);
			saveObjectSpawner(o.getObjectSpawner(), w);
			saveSpawnPoints(o.getSpawnPoints(), w);
			saveControlPoints(o.getControlPoints(), w);
			saveCombatArea(o.getCombatAreaManager(), w);

		} finally {
			w.flush();
			// w.close();
		}

		return;
	}

	private void saveGameLogic(GameLogic gl, BufferedWriter w)
			throws IOException {

		String prop;
		boolean hasProp = false;

		prop = gl.getSetTeamDropVehicle1();
		if (prop != null) {
			w.write("gameLogic.setTeamDropVehicle 1 " + prop);
			w.newLine();
			hasProp = true;
		}

		prop = gl.getSetTeamDropVehicle2();
		if (prop != null) {
			w.write("gameLogic.setTeamDropVehicle 2 " + prop);
			w.newLine();
			hasProp = true;
		}

		if (hasProp) {
			w.newLine();
		}

		return;
	}

	private void saveObjectSpawner(ObjectSpawner os, BufferedWriter w)
			throws IOException {

		w.write("rem ********** Object Spawner **********");
		w.newLine();

		/* saves templates */

		for (ObjectTemplate t : os.getTemplates()) {
			saveTemplate("ObjectSpawnerTemplate", "ObjectSpawner", t, w);
			w.newLine();
		}

		/* saves objects */

		w.write("if v_arg1 == host");
		w.newLine();

		w.newLine();

		for (Bf2Object o : os.getObjects()) {
			saveObject("ObjectSpawner", "   ", o, w);
			w.newLine();
		}

		w.write("endIf");
		w.newLine();

		w.newLine();

		return;
	}

	private void saveSpawnPoints(SpawnPoints sp, BufferedWriter w)
			throws IOException {

		w.write("rem ********** Spawn Points **********");
		w.newLine();

		int size = sp.getTemplates().size();
		for (int i = 0; i < size; i++) {

			ObjectTemplate t = sp.getTemplates().get(i);
			Bf2Object o = sp.getObjects().get(i);

			/* saves templates */
			saveTemplate("SpawnPointTemplate", "SpawnPoint", t, w);
			w.newLine();

			/* saves objects */
			saveObject("SpawnPoint", "", o, w);
			w.newLine();

		}

		return;
	}

	private void saveObject(String type, String indent, Bf2Object o,
			BufferedWriter w) throws IOException {

		/* makes a mapping from property name to conLine object */
		// Map<String, Bf2Property> lm = new HashMap<String, Bf2Property>();
		// for (Bf2Property p : o.getProperties()) {
		// String value = p.getValue();
		// if (value == null || value.length() == 0) {
		// /*
		// * ui may cause this
		// */
		// continue;
		// }
		// lm.put(p.getName(), p);
		// }
		Map<String, Bf2Property> lm = o.getProperties();

		Bf2Property prop;

		prop = lm.get("create");
		w.write(indent + "rem [" + type + ": " + prop + "]");
		w.newLine();

		writeObjectProperty(indent, "create", prop, w);

		for (String name : OBJECT_PROPERTIES) {
			prop = lm.get(name);
			writeObjectProperty(indent, name, prop, w);
		}

		return;
	}

	private void writeObjectProperty(String indent, String name,
			Bf2Property prop, BufferedWriter w) throws IOException {

		if (prop != null) {
			String value = prop.getValue();
			if (value != null && value.length() > 0) {
				if (prop.isEnabled()) {
					w.write(indent + "Object." + name + " " + value);
				} else {
					w.write(indent + "rem Object." + name + " " + value);
				}
				w.newLine();
			}
		}

		return;
	}

	private void saveControlPoints(ControlPoints cp, BufferedWriter w)
			throws IOException {

		w.write("rem ********** Control Points **********");
		w.newLine();

		/* saves templates */

		for (ObjectTemplate t : cp.getTemplates()) {
			saveTemplate("ControlPointTemplate", "ControlPoint", t, w);
			w.newLine();
		}

		/* saves objects */

		w.write("if v_arg1 == host");
		w.newLine();

		w.newLine();

		for (Bf2Object o : cp.getObjects()) {
			saveObject("ControlPoint", "   ", o, w);
			w.newLine();
		}

		w.write("endIf");
		w.newLine();

		w.newLine();

		return;
	}

	private void saveCombatArea(CombatAreaManager cam, BufferedWriter w)
			throws IOException {

		String prop;

		prop = cam.getUse();
		if (prop != null) {
			w.write("CombatAreaManager.use " + prop);
			w.newLine();
		}

		prop = cam.getDamage();
		if (prop != null) {
			w.write("CombatAreaManager.damage " + prop);
			w.newLine();
		}

		prop = cam.getTimeAllowedOutside();
		if (prop != null) {
			w.write("CombatAreaManager.timeAllowedOutside " + prop);
			w.newLine();
		}

		for (CombatArea ca : cam.getAreas()) {
			w.write("CombatArea.create " + ca.getCreate());
			w.newLine();

			w.write("CombatArea.min " + ca.getMin());
			w.newLine();

			w.write("CombatArea.max " + ca.getMax());
			w.newLine();

			for (String p : ca.getAreaPoints()) {

				w.write("CombatArea.addAreaPoint " + p);
				w.newLine();

			}

			w.write("CombatArea.team " + ca.getTeam());
			w.newLine();

			w.write("CombatArea.vehicles " + ca.getVehicles());
			w.newLine();

			w.write("CombatArea.layer " + ca.getLayer());
			w.newLine();

			prop = ca.getUsedByPathFinding();
			if (prop != null) {
				w.write("CombatArea.usedByPathFinding " + prop);
				w.newLine();
			}

			w.newLine();

		}

		return;
	}

	private void saveTemplate(String remark, String type, ObjectTemplate t,
			BufferedWriter w) throws IOException {

		/* makes a mapping from property name to conLine object */
		// Map<String, Bf2Property> lm = new HashMap<String, Bf2Property>();
		// for (Bf2Property p : t.getProperties()) {
		// String value = p.getValue();
		// if (value == null || value.length() == 0) {
		// /*
		// * ui may cause this
		// */
		// continue;
		// }
		// lm.put(p.getName(), p);
		// }
		Map<String, Bf2Property> lm = t.getProperties();

		Bf2Property prop;

		prop = lm.get("create " + type);
		if (remark != null) {
			w.write("rem [" + remark + ": " + prop + "]");
			w.newLine();
		}
		writeTemplateProperty("create " + type, prop, w);

		prop = lm.get("activeSafe " + type);
		writeTemplateProperty("activeSafe " + type, prop, w);

		for (String name : OBJECT_TEMPLATE_PROPERTIES_1) {
			prop = lm.get(name);
			writeTemplateProperty(name, prop, w);
		}

		prop = lm.get("addTemplate");
		if (prop != null) {

			w.write("rem -------------------------------------");
			w.newLine();

			w.write("ObjectTemplate." + "addTemplate " + prop);
			w.newLine();

			w.write("rem -------------------------------------");
			w.newLine();
		}

		for (String name : OBJECT_TEMPLATE_PROPERTIES_2) {
			prop = lm.get(name);
			writeTemplateProperty(name, prop, w);
		}

		return;
	}

	private void writeTemplateProperty(String name, Bf2Property prop,
			BufferedWriter w) throws IOException {

		if (prop != null) {
			String value = prop.getValue();
			if (value != null && value.length() > 0) {
				w.write("ObjectTemplate." + name + " " + value);
				w.newLine();
			}
		}

		return;
	}

	public Hashtable<String, GamePlayObject> getGamePlayObjects() {
		return gamePlayObjects;
	}
}
